#include <graphics/Texture.h>
#include <graphics/ImageManager.h>
#include <system/Log.h>

namespace ne
{
    Texture::Texture(ResourceManager *pManager,
                     const uint64_t id,
                     const std::string &name,
                     const std::string &fileName,
                     const bool isManual,
                     ManualResourceLoader *pLoader)
        : Resource(pManager, id, name, fileName, isManual, pLoader),
          GLObject(),
          mType(TT_2D),
          mFormat(TF_RGBA),
          mPixelType(PT_UNSIGNED_BYTE),
          mWidth(0),
          mHeight(0),
          mDepth(0),
          mNumMipmap(0),
          mMinFilter(TFF_LINEAR),
          mMagFilter(TFF_LINEAR),
          mMinLOD(-1000.0f),
          mMaxLOD(1000.0f),
          mLODBias(0.0f),
          mWrapS(TWP_CLAMP_TO_EDGE),
          mWrapT(TWP_CLAMP_TO_EDGE),
          mWrapR(TWP_CLAMP_TO_EDGE),
          mCompareMode(TCM_NONE),
          mCompareFunc(TCF_LEQUAL),
          mDepthTextureMode(TDTM_LUMINANCE),
          mCreated(false)
    {
    }

    void Texture::createInternalTexture(const TextureType type,
                                        const TextureFormat internalFormat,
                                        const PixelFormat format,
                                        const PixelType pt,
                                        const uint32_t width,
                                        const uint32_t height,
                                        const uint32_t depth,
                                        const void *data)
    {
        if (!mSource)
        {
            mType = type;
            mFormat = internalFormat;
            mPixelType = pt;
            mWidth = width;
            mHeight = height;
            mDepth = depth;

            glGenTextures(1, &mSource);
            glBindTexture(mType, mSource);

#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
            glPixelStorei(GL_UNPACK_ROW_LENGTH, mWidth);
            glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, mHeight);
#endif
            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

            switch (mType)
            {
                case TT_1D:
#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
                    glTexImage1D(mType, 0, mFormat, mWidth, 0,
                                 format, mPixelType, data);
#else
                    glTexImage2D(mType, 0, mFormat, mWidth, 1, 0,
                                 format, mPixelType, data);
#endif
                    break;

                case TT_2D:
                case TT_1D_ARRAY:
                    glTexImage2D(mType, 0, mFormat, mWidth, mHeight, 0,
                                 format, mPixelType, data);
                    break;

                case TT_3D:
                case TT_2D_ARRAY:
#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
                    glTexImage3D(mType, 0, mFormat, mWidth, mHeight, mDepth, 0,
                                 format, mPixelType, data);
#else
                    glTexImage2D(mType, 0, mFormat, mWidth, mHeight, 0,
                                 format, mPixelType, data);
#endif
                    break;

                case TT_CUBE_MAP:
                    for (int face=0; face<6; ++face)
                    {
                        glTexImage2D(TCMF_POSITIVE_X + face, 0,
                                     mFormat, mWidth, mHeight, 0,
                                     format, mPixelType, data);
                    }
                    break;

                default:
                    break;
            }

#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
            glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
            glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
#endif
            glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
        }
    }

    void Texture::freeInternalTexture()
    {
        if (mSource)
        {
            glDeleteTextures(1, &mSource);
        }
        mSource = 0;
        mType = TT_2D;
        mFormat = TF_RGBA;
        mPixelType = PT_UNSIGNED_BYTE;
        mWidth = 0;
        mHeight = 0;
        mDepth = 0;
        _needCreated();
    }

    // TODO: This function is wrong, need to fix later
    void Texture::createRenderTexture()
    {
        if (mSource && !mCreated)
        {
            glBindTexture(mType, mSource);

            glTexParameteri(mType, GL_TEXTURE_MIN_FILTER, mMinFilter);
            glTexParameteri(mType, GL_TEXTURE_MAG_FILTER, mMagFilter);

            glTexParameteri(mType, GL_TEXTURE_WRAP_S, mWrapS);
#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
            if (mType != TT_1D && mType != TT_1D_ARRAY)
            {
                glTexParameteri(mType, GL_TEXTURE_WRAP_T, mWrapT);
                if (mType != TT_2D && mType != TT_2D_ARRAY)
                {
                    glTexParameteri(mType, GL_TEXTURE_WRAP_R, mWrapR);
                }
            }

            if (mCompareMode == TCM_COMPARE_R_TO_TEXTURE)
            {
                glTexParameteri(mType, GL_TEXTURE_COMPARE_MODE, mCompareMode);
                glTexParameteri(mType, GL_TEXTURE_COMPARE_FUNC, mCompareFunc);
                glTexParameteri(mType, GL_DEPTH_TEXTURE_MODE, mDepthTextureMode);
            }

            if (mNumMipmap > 0)
            {
                glTexParameterf(mType, GL_TEXTURE_MIN_LOD, mMinLOD);
                glTexParameterf(mType, GL_TEXTURE_MAX_LOD, mMaxLOD);
                glTexParameterf(mType, GL_TEXTURE_LOD_BIAS, mLODBias);
                glTexParameteri(mType, GL_TEXTURE_BASE_LEVEL, 0);
                glTexParameteri(mType, GL_TEXTURE_MAX_LEVEL, mNumMipmap);
                glTexParameteri(mType, GL_GENERATE_MIPMAP, GL_TRUE);
                glGenerateMipmap(mType);
            }
            else
            {
                glTexParameteri(mType, GL_TEXTURE_BASE_LEVEL, 0);
                glTexParameteri(mType, GL_TEXTURE_MAX_LEVEL, 0);
                glTexParameteri(mType, GL_GENERATE_MIPMAP, GL_FALSE);
            }
#endif

            // glEnable/glDisable(GL_TEXTURE_CUBE_MAP_SEAMLESS)
            mCreated = true;
        }
    }

    void Texture::loadImage(const Image &image)
    {
        loadData(image.getData(), image.getFormat(), image.getPixelType(),
                 0, 0, 0,
                 image.getWidth(), image.getHeight(), image.getDepth());
    }

    void Texture::loadData(const void *data,
                           const PixelFormat format,
                           const PixelType pt,
                           const uint32_t x,
                           const uint32_t y,
                           const uint32_t z,
                           const uint32_t width,
                           const uint32_t height,
                           const uint32_t depth)
    {
        if (mSource && data)
        {
            glBindTexture(mType, mSource);

#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
            glPixelStorei(GL_UNPACK_ROW_LENGTH, width);
            glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, height);
#endif
            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

            switch (mType)
            {
                case TT_1D:
#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
                    glTexSubImage1D(mType, 0, x, width,
                                    format, pt, data);
#else
                    glTexSubImage2D(mType, 0, x, 0, width, 1,
                                    format, pt, data);
#endif
                    break;

                case TT_2D:
                case TT_1D_ARRAY:
                    glTexSubImage2D(mType, 0, x, y, width, height,
                                    format, pt, data);
                    break;

                case TT_3D:
                case TT_2D_ARRAY:
#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
                    glTexSubImage3D(mType, 0, x, y, z, width, height, depth,
                                    format, pt, data);
#else
                    glTexSubImage2D(mType, 0, x, y, width, height,
                                    format, pt, data);
#endif
                    break;

                default:
                    break;
            }

#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
            glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
            glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
#endif
            glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
        }
    }

    void Texture::loadImageForCubeMap(const TextureCubeMapFace face,
                                      const Image &image)
    {
        loadDataForCubeMap(face,
                           image.getData(),
                           image.getFormat(),
                           image.getPixelType(),
                           image.getWidth(),
                           image.getHeight());
    }

    void Texture::loadDataForCubeMap(const TextureCubeMapFace face,
                                     const void *data,
                                     const PixelFormat format,
                                     const PixelType pt,
                                     const uint32_t width,
                                     const uint32_t height)
    {
        if (mSource && mType == TT_CUBE_MAP && data)
        {
            glBindTexture(mType, mSource);

#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
            glPixelStorei(GL_UNPACK_ROW_LENGTH, width);
            glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, height);
#endif
            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

            glTexSubImage2D(face, 0, 0, 0, width, height,
                            format, pt, data);

#if NE_RENDER_SYSTEM == NE_RENDER_SYSTEM_OPENGL
            glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
            glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
#endif
            glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
        }
    }

    //TODO: Only can call this before createInternalTexture
    void Texture::setType(const TextureType type)
    {
        if (!mSource)
        {
            mType = type;
        }
    }

    TextureType Texture::getType() const
    {
        return mType;
    }

    //TODO: Only can call this before createInternalTexture
    void Texture::setFormat(const TextureFormat format)
    {
        if (!mSource)
        {
            mFormat = format;
        }
    }

    TextureFormat Texture::getFormat() const
    {
        return mFormat;
    }

    PixelType Texture::getPixelType() const
    {
        return mPixelType;
    }

    uint32_t Texture::getWidth() const
    {
        return mWidth;
    }

    uint32_t Texture::getHeight() const
    {
        return mHeight;
    }

    uint32_t Texture::getDepth() const
    {
        return mDepth;
    }

    void Texture::setNumMipmap(const uint32_t mip)
    {
        if (mNumMipmap != mip)
        {
            mNumMipmap = mip;
            _needCreated();
        }
    }

    uint32_t Texture::getNumMipmap() const
    {
        return mNumMipmap;
    }

    void Texture::setFilter(const TextureFilterFunc minFF, const TextureFilterFunc magFF)
    {
        mMinFilter = minFF;
        mMagFilter = magFF;
        _needCreated();
    }

    TextureFilterFunc Texture::getMinFilter() const
    {
        return mMinFilter;
    }

    TextureFilterFunc Texture::getMagFilter() const
    {
        return mMagFilter;
    }

    void Texture::setLOD(const real minLOD, const real maxLOD, const real bias)
    {
        mMinLOD = minLOD;
        mMaxLOD = maxLOD;
        mLODBias = bias;
        _needCreated();
    }

    real Texture::getMinLOD() const
    {
        return mMinLOD;
    }

    real Texture::getMaxLOD() const
    {
        return mMaxLOD;
    }

    real Texture::getLODBias() const
    {
        return mLODBias;
    }

    void Texture::setWrap(const TextureWrapParameter wpS,
                          const TextureWrapParameter wpT,
                          const TextureWrapParameter wpR)
    {
        mWrapS = wpS;
        mWrapT = wpT;
        mWrapR = wpR;
        _needCreated();
    }

    TextureWrapParameter Texture::getWrapS() const
    {
        return mWrapS;
    }

    TextureWrapParameter Texture::getWrapT() const
    {
        return mWrapT;
    }

    TextureWrapParameter Texture::getWrapR() const
    {
        return mWrapR;
    }

    void Texture::setDepthCompare(const bool enable,
                                  const TextureCompareFunc cf,
                                  const TextureDepthTextureMode dtm)
    {
        if (enable)
        {
            mCompareMode = TCM_COMPARE_R_TO_TEXTURE;
            mCompareFunc = cf;
            mDepthTextureMode = dtm;
        }
        else
        {
            mCompareMode = TCM_NONE;
            mCompareFunc = cf;
            mDepthTextureMode = dtm;
        }

        _needCreated();
    }

    TextureCompareMode Texture::getCompareMode() const
    {
        return mCompareMode;
    }

    TextureCompareFunc Texture::getCompareFunc() const
    {
        return mCompareFunc;
    }

    TextureDepthTextureMode Texture::getDepthTextureMode() const
    {
        return mDepthTextureMode;
    }

    bool Texture::isCreated() const
    {
        return mCreated;
    }

    Texture::~Texture()
    {
        unload();
    }

    bool Texture::loadImplement()
    {
        ResourceCreateOrRetrieveResult res = ImageManager::GetSingleton().createOrRetrieve(getName(), getFileName());
        ImagePtr pImage(res.first);
        if (pImage.isNull())
        {
            Log::Out(LL_WARNING,
                "Create Texture (%s) from file (%s) failed", getName().c_str(), getFileName().c_str());
            return false;
        }
        if (!pImage->isLoaded())
        {
            if (!pImage->load(isBackgroundThreadLoading()))
            {
                Log::Out(LL_WARNING,
                    "Load Image for Texture (%s) from file (%s) failed", getName().c_str(), getFileName().c_str());
                return false;
            }
        }

        mType = TT_1D;
        if (pImage->getHeight() > 1)
        {
            mType = TT_2D;
        }
        if (pImage->getDepth() > 1)
        {
            mType = TT_2D_ARRAY;
        }

        createInternalTexture(mType, mFormat, pImage->getFormat(), pImage->getPixelType(),
                              pImage->getWidth(), pImage->getHeight(), pImage->getDepth());
        loadImage(*pImage);

        //TODO: Auto remove the image if the image is created here
        if (res.second)
        {
            ImageManager::GetSingleton().removeByName(mName);
        }

        createRenderTexture();

        return true;
    }

    bool Texture::unloadImplement()
    {
        freeInternalTexture();
        return true;
    }

    bool Texture::downloadImplement()
    {
        if (mSource)
        {
            return true;
        }
        return false;
    }

    size_t Texture::_calculateSize() const
    {
        //TODO: Here need to be fixed
        return mHeight * mDepth * mWidth;
    }

    void Texture::_needCreated()
    {
        mCreated = false;
    }
}

